package xyz.apiote.bimba.czwek.api

import android.graphics.*
import android.os.Parcelable
import android.util.Log
import kotlinx.parcelize.Parcelize
import org.yaml.snakeyaml.Yaml
import xyz.apiote.fruchtfleisch.Reader
import java.io.InputStream
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.*
import kotlin.reflect.KClass

class TrafficFormatException(override val message: String) : IllegalArgumentException()
class UnknownResourceVersionException(resource: String, val version: ULong) :
	Exception("unknown version $version of $resource")

class UnknownResourceException(resource: String, cls: KClass<*>) :
	Exception("unknown class $cls for $resource")

data class BimbaInfo(
	val contact: Map<String, String>,
)

data class Bimba(
	val info: BimbaInfo,
	val servers: List<Map<String, String>>,
	val security: List<Map<String, List<*>>>
) {
	companion object {
		@Suppress("UNCHECKED_CAST")
		fun unmarshal(stream: InputStream): Bimba {
			val map = Yaml().load(stream) as HashMap<String, *>
			val contact =
				if (map["info"] is Map<*, *> && (map["info"] as Map<*, *>).keys.all { it is String }) {
					(map["info"] as Map<String, *>)["contact"]
				} else {
					throw TrafficFormatException("invalid info format")
				}
			val contactMap =
				if (contact is Map<*, *> && contact.all { it.key is String && it.value is String }) {
					contact as Map<String, String>
				} else {
					throw TrafficFormatException("invalid contact format")
				}
			val servers = if (map["servers"] is List<*> && (map["servers"] as List<*>).all { server ->
					server is Map<*, *> && server.all { it.key is String && it.value is String }
				}) {
				map["servers"] as List<Map<String, String>>
			} else {
				throw TrafficFormatException("invalid servers format")
			}
			val security =
				if (map["security"] is List<*> && (map["security"] as List<*>).all { security ->
						security is Map<*, *> && security.all { it.key is String && it.value is List<*> }
					}) {
					map["security"] as List<Map<String, List<*>>>
				} else {
					throw TrafficFormatException("invalid security format")
				}
			val bimba = Bimba(BimbaInfo(contactMap), servers, security)
			bimba.validate()
			return bimba
		}
	}

	fun isPrivate(): Boolean {
		return security.size == 1 && security[0]["api_key"] != null
	}

	fun isRateLimited(): Boolean {
		val items = security.foldRight(0b00) { map, acc ->
			acc or when {
				map.containsKey("api_key") -> 0b10
				map.isEmpty() -> 0b01
				else -> 0b00
			}
		}
		Log.i("Rate limited", "${this}, $items")
		return security.size == 2 && items == 0b11
	}

	private fun validate() {
		if (servers.isEmpty() || servers[0]["url"] == null) {
			throw TrafficFormatException("no server in info")
		}
		if (security.isEmpty()) {
			throw TrafficFormatException("no security")
		}
	}
}

@Parcelize
data class PositionV1(
	val latitude: Double, val longitude: Double
) : Parcelable {

	override fun toString(): String = "$latitude,$longitude"


	companion object {
		fun unmarshal(stream: InputStream): PositionV1 {
			val reader = Reader(stream)
			return PositionV1(
				reader.readFloat64(), reader.readFloat64()
			)
		}
	}
}

data class FeedInfoV1(
	val name: String,
	val id: String,
	val attribution: String,
	val description: String,
	val lastUpdate: ZonedDateTime
) {
	companion object {
		fun unmarshal(stream: InputStream): FeedInfoV1 {
			val reader = Reader(stream)
			return FeedInfoV1(
				reader.readString(),
				reader.readString(),
				reader.readString(),
				reader.readString(),
				ZonedDateTime.parse(reader.readString(), DateTimeFormatter.ISO_DATE_TIME)
			)
		}
	}

	fun formatDate(): String {
		return lastUpdate.format(
			DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(Locale.getDefault())
		)
	}
}

enum class AlertCauseV1 {
	UNKNOWN, OTHER, TECHNICAL_PROBLEM, STRIKE, DEMONSTRATION, ACCIDENT, HOLIDAY, WEATHER, MAINTENANCE,
	CONSTRUCTION, POLICE_ACTIVITY, MEDICAL_EMERGENCY;

	companion object {
		fun of(type: UInt): AlertCauseV1 {
			return when (type) {
				0u -> valueOf("UNKNOWN")
				1u -> valueOf("OTHER")
				2u -> valueOf("TECHNICAL_PROBLEM")
				3u -> valueOf("STRIKE")
				4u -> valueOf("DEMONSTRATION")
				5u -> valueOf("ACCIDENT")
				6u -> valueOf("HOLIDAY")
				7u -> valueOf("WEATHER")
				8u -> valueOf("MAINTENANCE")
				9u -> valueOf("CONSTRUCTION")
				10u -> valueOf("POLICE_ACTIVITY")
				11u -> valueOf("MEDICAL_EMERGENCY")
				else -> throw UnknownResourceVersionException("AlertCause/$type", 1u)
			}
		}
	}
}

enum class AlertEffectV1 {
	UNKNOWN, OTHER, NO_SERVICE, REDUCED_SERVICE, SIGNIFICANT_DELAYS, DETOUR, ADDITIONAL_SERVICE,
	MODIFIED_SERVICE, STOP_MOVED, NONE, ACCESSIBILITY_ISSUE;

	companion object {
		fun of(type: UInt): AlertEffectV1 {
			return when (type) {
				0u -> valueOf("UNKNOWN")
				1u -> valueOf("OTHER")
				2u -> valueOf("NO_SERVICE")
				3u -> valueOf("REDUCED_SERVICE")
				4u -> valueOf("SIGNIFICANT_DELAYS")
				5u -> valueOf("DETOUR")
				6u -> valueOf("ADDITIONAL_SERVICE")
				7u -> valueOf("MODIFIED_SERVICE")
				8u -> valueOf("STOP_MOVED")
				9u -> valueOf("NONE")
				10u -> valueOf("ACCESSIBILITY_ISSUE")
				else -> throw UnknownResourceVersionException("AlertEffect/$type", 1u)
			}
		}
	}
}

data class AlertV1(
	val header: String,
	val Description: String,
	val Url: String,
	val Cause: AlertCauseV1,
	val Effect: AlertEffectV1
) {
	companion object {
		fun unmarshal(stream: InputStream): AlertV1 {
			val reader = Reader(stream)
			return AlertV1(
				reader.readString(),
				reader.readString(),
				reader.readString(),
				AlertCauseV1.of(reader.readUInt().toULong().toUInt()),
				AlertEffectV1.of(reader.readUInt().toULong().toUInt())
			)
		}
	}
}

data class Time(
	val Hour: UInt, val Minute: UInt, val Second: UInt, val DayOffset: Byte, val Zone: String
) {
	companion object {
		fun unmarshal(stream: InputStream): Time {
			val reader = Reader(stream)
			return Time(
				reader.readUInt().toULong().toUInt(),
				reader.readUInt().toULong().toUInt(),
				reader.readUInt().toULong().toUInt(),
				reader.readI8(),
				reader.readString()
			)
		}
	}
}

data class ColourV1(val R: UByte, val G: UByte, val B: UByte) {
	companion object {
		fun unmarshal(stream: InputStream): ColourV1 {
			val reader = Reader(stream)
			return ColourV1(
				reader.readU8(), reader.readU8(), reader.readU8()
			)
		}
	}
}

enum class CongestionLevelV1 {
	UNKNOWN, SMOOTH, STOP_AND_GO, SIGNIFICANT, SEVERE;

	companion object {
		fun of(type: UInt): CongestionLevelV1 {
			return when (type) {
				0u -> valueOf("UNKNOWN")
				1u -> valueOf("SMOOTH")
				2u -> valueOf("STOP_AND_GO")
				3u -> valueOf("SIGNIFICANT")
				4u -> valueOf("SEVERE")
				else -> throw UnknownResourceVersionException("CongestionLevel/$type", 1u)
			}
		}
	}
}

enum class OccupancyStatusV1 {
	UNKNOWN, EMPTY, MANY_AVAILABLE, FEW_AVAILABLE, STANDING_ONLY, CRUSHED, FULL, NOT_ACCEPTING;

	companion object {
		fun of(type: UInt): OccupancyStatusV1 {
			return when (type) {
				0u -> valueOf("UNKNOWN")
				1u -> valueOf("EMPTY")
				2u -> valueOf("MANY_AVAILABLE")
				3u -> valueOf("FEW_AVAILABLE")
				4u -> valueOf("STANDING_ONLY")
				5u -> valueOf("CRUSHED")
				6u -> valueOf("FULL")
				7u -> valueOf("NOT_ACCEPTING")
				else -> throw UnknownResourceVersionException("OccupancyStatus/$type", 1u)
			}
		}
	}
}

data class VehicleV1(
	val ID: String,
	val Position: PositionV1,
	val Capabilities: UShort,
	val Speed: Float,
	val Line: LineStubV1,
	val Headsign: String,
	val CongestionLevel: CongestionLevelV1,
	val OccupancyStatus: OccupancyStatusV1
) : LocatableV1 {
	companion object {
		fun unmarshal(stream: InputStream): VehicleV1 {
			val reader = Reader(stream)
			return VehicleV1(
				reader.readString(),
				PositionV1.unmarshal(stream),
				reader.readU16(),
				reader.readFloat32(),
				LineStubV1.unmarshal(stream),
				reader.readString(),
				CongestionLevelV1.of(reader.readUInt().toULong().toUInt()),
				OccupancyStatusV1.of(reader.readUInt().toULong().toUInt())
			)
		}
	}
}

data class VehicleV2(
	val ID: String,
	val Position: PositionV1,
	val Capabilities: UShort,
	val Speed: Float,
	val Line: LineStubV2,
	val Headsign: String,
	val CongestionLevel: CongestionLevelV1,
	val OccupancyStatus: OccupancyStatusV1
) : LocatableV2 {
	companion object {
		fun unmarshal(stream: InputStream): VehicleV2 {
			val reader = Reader(stream)
			return VehicleV2(
				reader.readString(),
				PositionV1.unmarshal(stream),
				reader.readU16(),
				reader.readFloat32(),
				LineStubV2.unmarshal(stream),
				reader.readString(),
				CongestionLevelV1.of(reader.readUInt().toULong().toUInt()),
				OccupancyStatusV1.of(reader.readUInt().toULong().toUInt())
			)
		}
	}
}

data class LineStubV1(
	val name: String, val kind: LineTypeV1, val colour: ColourV1
) {
	companion object {
		fun unmarshal(stream: InputStream): LineStubV1 {
			val reader = Reader(stream)
			return LineStubV1(
				reader.readString(),
				LineTypeV1.of(reader.readUInt().toULong().toUInt()),
				ColourV1.unmarshal(stream)
			)
		}
	}
}

data class LineStubV2(
	val name: String, val kind: LineTypeV2, val colour: ColourV1
) {
	companion object {
		fun unmarshal(stream: InputStream): LineStubV2 {
			val reader = Reader(stream)
			return LineStubV2(
				reader.readString(),
				LineTypeV2.of(reader.readUInt().toULong().toUInt()),
				ColourV1.unmarshal(stream)
			)
		}
	}
}

data class DepartureV1(
	val ID: String,
	val time: Time,
	val status: ULong,
	val isRealtime: Boolean,
	val vehicle: VehicleV1,
	val boarding: UByte
) {

	companion object {
		fun unmarshal(stream: InputStream): DepartureV1 {
			val reader = Reader(stream)
			val id = reader.readString()
			val time = Time.unmarshal(stream)
			val status = reader.readUInt().toULong()
			val isRealtime = reader.readBoolean()
			val vehicle = VehicleV1.unmarshal(stream)
			val boarding = reader.readU8()
			return DepartureV1(id, time, status, isRealtime, vehicle, boarding)
		}
	}
}

data class DepartureV2(
	val ID: String,
	val time: Time,
	val status: ULong,
	val isRealtime: Boolean,
	val vehicle: VehicleV2,
	val boarding: UByte
) {

	companion object {
		fun unmarshal(stream: InputStream): DepartureV2 {
			val reader = Reader(stream)
			val id = reader.readString()
			val time = Time.unmarshal(stream)
			val status = reader.readUInt().toULong()
			val isRealtime = reader.readBoolean()
			val vehicle = VehicleV2.unmarshal(stream)
			val boarding = reader.readU8()
			return DepartureV2(id, time, status, isRealtime, vehicle, boarding)
		}
	}
}

@Parcelize
data class StopV2(
	val code: String,
	val name: String,
	val nodeName: String,
	val zone: String,
	val feedID: String,
	val position: PositionV1,
	val changeOptions: List<ChangeOptionV1>
) : QueryableV2, Parcelable, LocatableV2 {
	companion object {
		fun unmarshal(stream: InputStream): StopV2 {
			val reader = Reader(stream)
			val code = reader.readString()
			val name = reader.readString()
			val nodeName = reader.readString()
			val zone = reader.readString()
			val feedID = reader.readString()
			val position = PositionV1.unmarshal(stream)
			val chOptionsNum = reader.readUInt().toULong()
			val changeOptions = mutableListOf<ChangeOptionV1>()
			for (i in 0UL until chOptionsNum) {
				changeOptions.add(ChangeOptionV1.unmarshal(stream))
			}
			return StopV2(
				name = name,
				nodeName = nodeName,
				code = code,
				zone = zone,
				position = position,
				feedID = feedID,
				changeOptions = changeOptions
			)
		}
	}
}

@Parcelize
data class StopV1(
	val code: String,
	val name: String,
	val zone: String,
	val position: PositionV1,
	val changeOptions: List<ChangeOptionV1>
) : QueryableV1, Parcelable, LocatableV1 {
	companion object {
		fun unmarshal(stream: InputStream): StopV1 {
			val reader = Reader(stream)
			val code = reader.readString()
			val name = reader.readString()
			val zone = reader.readString()
			val position = PositionV1.unmarshal(stream)
			val chOptionsNum = reader.readUInt().toULong()
			val changeOptions = mutableListOf<ChangeOptionV1>()
			for (i in 0UL until chOptionsNum) {
				changeOptions.add(ChangeOptionV1.unmarshal(stream))
			}
			return StopV1(
				name = name, code = code, zone = zone, position = position, changeOptions = changeOptions
			)
		}
	}
}

data class LineV1(
	val name: String,
	val colour: ColourV1,
	val type: LineTypeV2,
	val feedID: String,
	val headsigns: List<List<String>>,
	val graphs: List<LineGraph>,
) : QueryableV2 {
	override fun toString(): String {
		return "$name ($type) [$colour]\n${headsigns.map { "-> ${it.joinToString()}" }}"
	}

	companion object {
		fun unmarshal(stream: InputStream): LineV1 {
			val reader = Reader(stream)
			val name = reader.readString()
			val colour = ColourV1.unmarshal(stream)
			val type = reader.readUInt()
			val feedID = reader.readString()
			var directionsNum = reader.readUInt().toULong()
			val headsigns = (0UL until directionsNum).map {
				val headsignsNum = reader.readUInt().toULong()
				val headsignsDir = mutableListOf<String>()
				for (j in 0UL until headsignsNum) {
					headsignsDir.add(reader.readString())
				}
				headsignsDir
			}
			directionsNum = reader.readUInt().toULong()
			val graphs = mutableListOf<LineGraph>()
			for (i in 0UL until directionsNum) {
				graphs.add(LineGraph.unmarshal(stream))
			}
			return LineV1(
				name = name,
				colour = colour,
				type = LineTypeV2.of(type.toULong().toUInt()),
				feedID = feedID,
				headsigns = headsigns,
				graphs = graphs
			)
		}
	}
}

enum class LineTypeV1 {
	UNKNOWN, TRAM, BUS;

	companion object {
		fun of(type: UInt): LineTypeV1 {
			return when (type) {
				0u -> valueOf("UNKNOWN")
				1u -> valueOf("TRAM")
				2u -> valueOf("BUS")
				else -> throw UnknownResourceVersionException("LineType/$type", 1u)
			}
		}
	}
}

enum class LineTypeV2 {
	UNKNOWN, TRAM, BUS, TROLLEYBUS;

	companion object {
		fun of(type: UInt): LineTypeV2 {
			return when (type) {
				0u -> valueOf("UNKNOWN")
				1u -> valueOf("TRAM")
				2u -> valueOf("BUS")
				3u -> valueOf("TROLLEYBUS")
				else -> throw UnknownResourceVersionException("LineType/$type", 1u)
			}
		}
	}
}

@Parcelize
data class ChangeOptionV1(val line: String, val headsign: String) : Parcelable {
	companion object {
		fun unmarshal(stream: InputStream): ChangeOptionV1 {
			val reader = Reader(stream)
			return ChangeOptionV1(line = reader.readString(), headsign = reader.readString())
		}
	}
}

data class LineGraph(
	val stops: List<StopStub>, val nextNodes: Map<Long, List<Long>>
) {
	companion object {
		fun unmarshal(stream: InputStream): LineGraph {
			val reader = Reader(stream)
			val stopsNum = reader.readUInt().toULong()
			val stops = mutableListOf<StopStub>()
			for (i in 0UL until stopsNum) {
				stops.add(StopStub.unmarshal(stream))
			}
			val nextNodesNum = reader.readUInt().toULong()
			val nextNodes = mutableMapOf<Long, List<Long>>()
			for (i in 0UL until nextNodesNum) {
				val from = reader.readInt().toLong()
				val toNum = reader.readUInt().toULong()
				val to = mutableListOf<Long>()
				for (j in 0UL until toNum) {
					to.add(reader.readInt().toLong())
				}
				nextNodes[from] = to
			}

			return LineGraph(stops = stops, nextNodes = nextNodes)
		}
	}
}

data class StopStub(
	val code: String, val name: String, val nodeName: String, val zone: String, val onDemand: Boolean
) {
	companion object {
		fun unmarshal(stream: InputStream): StopStub {
			val reader = Reader(stream)
			return StopStub(
				code = reader.readString(),
				name = reader.readString(),
				nodeName = reader.readString(),
				zone = reader.readString(),
				onDemand = reader.readBoolean()
			)
		}
	}
}